﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Microsoft.Build.Utilities;
using Microsoft.Win32;
using System.Reflection;
using System.Security.Cryptography;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;

namespace CommonTools
{
    public class AssemblyHelper : CommandLineHelper
    {
        private string _ilasmPath = "";
        public string ILAsmPath
        {
            get
            {
                if (string.IsNullOrEmpty(_ilasmPath))
                    _ilasmPath = ToolLocationHelper.GetPathToDotNetFrameworkFile("ilasm.exe", SupportedFramework);

                return _ilasmPath;
            }
            set { _ilasmPath = value; }
        }

        private string _ildasmPath = "";
        public string ILDasmPath
        {
            get
            {
                if (string.IsNullOrEmpty(_ildasmPath))
                    _ildasmPath = ToolLocationHelper.GetPathToDotNetFrameworkSdkFile("ildasm.exe", SupportedFramework);

                return _ildasmPath;
            }
            set { _ildasmPath = value; }
        }

        private string _snPath = "";
        public string SnPath
        {
            get
            {
                if (string.IsNullOrEmpty(_snPath))
                    _snPath = ToolLocationHelper.GetPathToDotNetFrameworkSdkFile("sn.exe", SupportedFramework);

                return _snPath;
            }
            set { _snPath = value; }
        }

        private TargetDotNetFrameworkVersion _targetFramework = TargetDotNetFrameworkVersion.VersionLatest;
        public TargetDotNetFrameworkVersion TargetFramework
        {
            get { return _targetFramework; }
            set
            {
                _targetFramework = value;
                _ilasmPath = _ildasmPath = _snPath = "";
            }
        }

        public TargetDotNetFrameworkVersion SupportedFramework
        {
            get { return GetSupportedFrameworkVersion(TargetFramework); }
        }

        public static TargetDotNetFrameworkVersion GetSupportedFrameworkVersion(TargetDotNetFrameworkVersion targetFW)
        {
            if (targetFW >= TargetDotNetFrameworkVersion.Version40)
                return TargetDotNetFrameworkVersion.Version40;
            else if (targetFW >= TargetDotNetFrameworkVersion.Version20)
                return TargetDotNetFrameworkVersion.Version20;
            else
                return TargetDotNetFrameworkVersion.Version11;
        }

        public static Dictionary<TargetDotNetFrameworkVersion, string> GetSupportedFrameworkVersions()
        {
            var supportedVersions = new Dictionary<TargetDotNetFrameworkVersion, string>();

            foreach (var version in GetInstalledFrameworkVersions())
                if (GetSupportedFrameworkVersion(version.Key) == version.Key)
                    supportedVersions.Add(version.Key, version.Value);

            return supportedVersions;
        }

        public static Dictionary<TargetDotNetFrameworkVersion, string> GetInstalledFrameworkVersions()
        {
            var installedVersions = new Dictionary<TargetDotNetFrameworkVersion, string>();

            var versionNumbers = new List<string>();

            using (RegistryKey ndpKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP"))
            {
                foreach (string versionKeyName in ndpKey.GetSubKeyNames())
                {
                    if (versionKeyName.StartsWith("v"))
                    {
                        string versionNumber = versionKeyName.Remove(0, 1);

                        if (versionNumber.StartsWith("4"))
                        {
                            var versionKey = ndpKey.OpenSubKey(versionKeyName);
                            var profileKey = versionKey.OpenSubKey("Full") ?? versionKey.OpenSubKey("Client");
                            versionNumber = profileKey.GetValue("Version").ToString();
                            
                            versionKey.Dispose();
                            profileKey.Dispose();
                        }

                        versionNumber = versionNumber.Substring(0, 3);
                        versionNumbers.Add(versionNumber);
                    }
                }
            }

            versionNumbers.Sort();

            foreach (string versionNumber in versionNumbers)
            {
                var targetFrameworkVersion = TargetFrameworkVersionFromString(versionNumber);
                if (targetFrameworkVersion != null && !installedVersions.ContainsKey(targetFrameworkVersion.Value))
                    installedVersions.Add(targetFrameworkVersion.Value, versionNumber);
            }

            return installedVersions;
        }

        public static TargetDotNetFrameworkVersion? TargetFrameworkVersionFromString(string versionNumber)
        {
            foreach (var targetFrameworkEnum in Enum.GetValues(typeof(TargetDotNetFrameworkVersion)))
            {
                string targetFrameworkNumber = targetFrameworkEnum.ToString().Replace("Version", "");
                if (versionNumber.Substring(0, 3).Replace(".", "") == targetFrameworkNumber)
                    return (TargetDotNetFrameworkVersion)targetFrameworkEnum;
            }
            return null;
        }

        public void CreateKeyPair(string keyFileName)
        {
            AddToOutput("Creating new key file " + keyFileName);
            Execute(SnPath, "-k " + Quote(keyFileName));
        }

        public void ILDasm(string assemblyPath, string ilFilePath = "")
        {
            AddToOutput("Disassembling " + assemblyPath);
            Execute(ILDasmPath, Quote(assemblyPath) + @" /out=" + Quote(ilFilePath) + " /all" + " /nobar" + " /text");
        }

        public void ILAsm(string ilFileName, string outAssemblyPath = "", string keyPath = "", string extension = "exe")
        {
            AddToOutput("Assembling " + ilFileName);
            Execute(ILAsmPath, Quote(ilFileName) + " /key=" + Quote(keyPath) + @" /out=" + Quote(outAssemblyPath) + @" /" + extension);
        }

        public string Sign(string assemblyPath, string keyPath, string outPath, bool backup = true)
        {
            ClearOutput();
            
            string assemblyName = Path.GetFileName(assemblyPath);
            string extension = Path.GetExtension(assemblyName).Replace(".", "");

            AddToOutput(string.Format("Assembly {0} processing started", assemblyName));

            if (string.IsNullOrEmpty(outPath)) outPath = Path.GetDirectoryName(assemblyPath);

            string outAssemblyPath = Path.Combine(outPath, assemblyName);

            string tempDir = Path.Combine(Path.GetTempPath() + Path.GetRandomFileName());

            try
            {
                Directory.CreateDirectory(tempDir);

                string ilFilePath = Path.Combine(tempDir, Path.ChangeExtension(assemblyName, ".il"));
                string tempAssemblyPath = Path.Combine(tempDir, assemblyName);

                ILDasm(assemblyPath, ilFilePath);

                ILAsm(ilFilePath, tempAssemblyPath, keyPath, extension);

                // if file with same name exists, change its extension to .unsigned
                if (backup && File.Exists(outAssemblyPath))
                {
                    string unsignedFileName = Path.Combine(outPath,
                        Path.GetFileNameWithoutExtension(assemblyName) + ".unsigned" + Path.GetExtension(assemblyName));

                    if (File.Exists(unsignedFileName)) File.Delete(unsignedFileName);

                    File.Move(outAssemblyPath, unsignedFileName);
                }

                // copy new assembly from temp to out directory
                File.Copy(tempAssemblyPath, outAssemblyPath, true);
            }
            catch (Exception ex)
            {
                Error += string.Format("\r\nAssembly {0} \r\n Error: {1}", assemblyPath, ex.Message);
                outAssemblyPath = "";
            }
            finally
            {
                if (Directory.Exists(tempDir)) 
                    Directory.Delete(tempDir, true);
            }

            AddToOutput(string.Format("Assembly {0} processing finished", assemblyName));

            return outAssemblyPath;
        }

        [DllImport("mscoree.dll", CharSet = CharSet.Unicode)]
        static extern bool StrongNameSignatureVerificationEx(string wszFilePath, bool fForceVerification, ref bool pfWasVerified);

        public static bool VerifyStrongName(string assemblyFile)
        {
            bool result = false;
            StrongNameSignatureVerificationEx(assemblyFile, true, ref result);
            return result;
        }

        public string SnVerify(string assemblyFile, bool quiet = true)
        {
            ClearOutput();
            
            Execute(SnPath, (quiet ? "-q " : "") + "-vf " + Quote(assemblyFile));
            return Output;
        }

        public static string ToHexString(byte[] bytes)
        {
            return BitConverter.ToString(bytes).Replace("-", "").ToLower();
        }

        public static string GetPublicKeyToken(string snkFilePath)
        {
            using (FileStream fs = File.OpenRead(snkFilePath))
            {
                StrongNameKeyPair snkPair = new StrongNameKeyPair(fs);
                return GetPublicKeyToken(snkPair.PublicKey);
            }
        }

        public static string GetPublicKeyToken(byte[] publicKey)
        {
            byte[] publicKeyToken = new byte[8];

            using (SHA1 sha1 = new SHA1Managed())
            {
                // get SHA1 hash (20 bytes)
                byte[] hash = sha1.ComputeHash(publicKey);
                // get last 8 bytes
                Array.Copy(hash, hash.Length - publicKeyToken.Length, publicKeyToken, 0, publicKeyToken.Length);
            }

            // reverse extracted bytes
            Array.Reverse(publicKeyToken, 0, publicKeyToken.Length);

            return ToHexString(publicKeyToken);
        }
        
        public static AssemblyInfo GetAssemblyInfo(string assemblyPath, bool lite = false)
        {
            AssemblyInfo assemblyInfo = null;

            if (lite)
            {
                var assemblyName = AssemblyName.GetAssemblyName(assemblyPath);
                assemblyInfo = new AssemblyInfo(assemblyName);
            }
            else
            {
                var assembly = Assembly.Load(File.ReadAllBytes(assemblyPath));
                    //Assembly.LoadFrom(assemblyPath);//
                assemblyInfo = new AssemblyInfo(assembly);
            }

            //assemblyInfo.IsSigned = VerifyStrongName(assemblyPath);

            return assemblyInfo;
        }

        public static bool IsValidClrAssembly(string assemblyFile)
        {
            string ext = Path.GetExtension(assemblyFile);

            if (ext == ".dll" || ext == ".exe")
            {
                try
                {
                    var assemblyName = AssemblyName.GetAssemblyName(assemblyFile);
                    return true;
                }
                catch (BadImageFormatException)
                {
                }
            }

            return false;
        }
    }

    [Serializable]
    public class AssemblyInfo
    {
        public string Name { get; set; }
        public string Version { get; set; }
        public string RuntimeVersion { get; set; }
        public string Platform { get; set; }
        public string PublicKeyToken { get; set; }
        public bool IsSigned { get; set; }

        public AssemblyInfo()
        {
        }

        public AssemblyInfo(AssemblyName assemblyName)
        {
            Name = assemblyName.Name;
            Version = assemblyName.Version.ToString();
            RuntimeVersion = "";
            Platform = assemblyName.ProcessorArchitecture.ToString();
            PublicKeyToken = AssemblyHelper.ToHexString(assemblyName.GetPublicKeyToken());
            IsSigned = PublicKeyToken != "";
        }

        public AssemblyInfo(Assembly assembly)
            : this(assembly.GetName())
        {
            RuntimeVersion = assembly.ImageRuntimeVersion;
        }
    }
}